﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Threading;
using HandyControl.Data;
using HandyControl.Tools;
using HandyControl.Tools.Interop;

namespace HandyControl.Controls;

public class NotifyIcon : FrameworkElement, IDisposable
{
    private bool _isMouseOver;

    private bool _added;

    private readonly object _syncObj = new();

    private readonly int _id;

    private ImageSource _icon;

    private IntPtr _iconCurrentHandle;

    private IntPtr _iconDefaultHandle;

    private IconHandle _iconHandle;

    private const int WmTrayMouseMessage = InteropValues.WM_USER + 1024;

    private string _windowClassName;

    private int _wmTaskbarCreated;

    private IntPtr _messageWindowHandle;

    private readonly InteropValues.WndProc _callback;

    private Popup _contextContent;

    private bool _doubleClick;

    private DispatcherTimer _dispatcherTimerBlink;

    private DispatcherTimer _dispatcherTimerPos;

    private bool _isTransparent;

    private bool _isDisposed;

    private static int NextId;

    private static readonly Dictionary<string, NotifyIcon> NotifyIconDic = new();

    static NotifyIcon()
    {
        VisibilityProperty.OverrideMetadata(typeof(NotifyIcon), new PropertyMetadata(Visibility.Visible, OnVisibilityChanged));
        DataContextProperty.OverrideMetadata(typeof(NotifyIcon), new FrameworkPropertyMetadata(DataContextPropertyChanged));
        ContextMenuProperty.OverrideMetadata(typeof(NotifyIcon), new FrameworkPropertyMetadata(ContextMenuPropertyChanged));
    }

    private static void OnVisibilityChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var ctl = (NotifyIcon) d;
        var v = (Visibility) e.NewValue;

        if (v == Visibility.Visible)
        {
            if (ctl._iconCurrentHandle == IntPtr.Zero)
            {
                ctl.OnIconChanged();
            }
            ctl.UpdateIcon(true);
        }
        else if (ctl._iconCurrentHandle != IntPtr.Zero)
        {
            ctl.UpdateIcon(false);
        }
    }

    private static void DataContextPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) =>
        ((NotifyIcon) d).OnDataContextPropertyChanged(e);

    private void OnDataContextPropertyChanged(DependencyPropertyChangedEventArgs e)
    {
        UpdateDataContext(_contextContent, e.OldValue, e.NewValue);
        UpdateDataContext(ContextMenu, e.OldValue, e.NewValue);
    }

    private static void ContextMenuPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var ctl = (NotifyIcon) d;
        ctl.OnContextMenuPropertyChanged(e);
    }

    private void OnContextMenuPropertyChanged(DependencyPropertyChangedEventArgs e) =>
        UpdateDataContext((ContextMenu) e.NewValue, null, DataContext);

    public NotifyIcon()
    {
        _id = ++NextId;
        _callback = Callback;

        Loaded += (s, e) => Init();

        if (Application.Current != null) Application.Current.Exit += (s, e) => Dispose();
    }

    ~NotifyIcon() => Dispose(false);

    public void Init()
    {
        RegisterClass();
        if (Visibility == Visibility.Visible)
        {
            OnIconChanged();
            UpdateIcon(true);
        }

        _dispatcherTimerPos = new DispatcherTimer
        {
            Interval = TimeSpan.FromMilliseconds(200)
        };
        _dispatcherTimerPos.Tick += DispatcherTimerPos_Tick;
    }

    public static void Register(string token, NotifyIcon notifyIcon)
    {
        if (string.IsNullOrEmpty(token) || notifyIcon == null) return;
        NotifyIconDic[token] = notifyIcon;
    }

    public static void Unregister(string token, NotifyIcon notifyIcon)
    {
        if (string.IsNullOrEmpty(token) || notifyIcon == null) return;

        if (NotifyIconDic.ContainsKey(token))
        {
            if (ReferenceEquals(NotifyIconDic[token], notifyIcon))
            {
                NotifyIconDic.Remove(token);
            }
        }
    }

    public static void Unregister(NotifyIcon notifyIcon)
    {
        if (notifyIcon == null) return;
        var first = NotifyIconDic.FirstOrDefault(item => ReferenceEquals(notifyIcon, item.Value));
        if (!string.IsNullOrEmpty(first.Key))
        {
            NotifyIconDic.Remove(first.Key);
        }
    }

    public static void Unregister(string token)
    {
        if (string.IsNullOrEmpty(token)) return;

        if (NotifyIconDic.ContainsKey(token))
        {
            NotifyIconDic.Remove(token);
        }
    }

    public static void ShowBalloonTip(string title, string content, NotifyIconInfoType infoType, string token)
    {
        if (NotifyIconDic.TryGetValue(token, out var notifyIcon))
        {
            notifyIcon.ShowBalloonTip(title, content, infoType);
        }
    }

    public void ShowBalloonTip(string title, string content, NotifyIconInfoType infoType)
    {
        if (!_added || DesignerHelper.IsInDesignMode) return;

        var data = new InteropValues.NOTIFYICONDATA
        {
            uFlags = InteropValues.NIF_INFO,
            hWnd = _messageWindowHandle,
            uID = _id,
            szInfoTitle = title ?? string.Empty,
            szInfo = content ?? string.Empty
        };

        data.dwInfoFlags = infoType switch
        {
            NotifyIconInfoType.Info => InteropValues.NIIF_INFO,
            NotifyIconInfoType.Warning => InteropValues.NIIF_WARNING,
            NotifyIconInfoType.Error => InteropValues.NIIF_ERROR,
            NotifyIconInfoType.None => InteropValues.NIIF_NONE,
            _ => data.dwInfoFlags
        };

        InteropMethods.Shell_NotifyIcon(InteropValues.NIM_MODIFY, data);
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    public void CloseContextControl()
    {
        if (_contextContent != null)
        {
            _contextContent.IsOpen = false;
        }
        else if (ContextMenu != null)
        {
            ContextMenu.IsOpen = false;
        }
    }

    public static readonly DependencyProperty TokenProperty = DependencyProperty.Register(
        nameof(Token), typeof(string), typeof(NotifyIcon), new PropertyMetadata(default(string), OnTokenChanged));

    private static void OnTokenChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (d is NotifyIcon notifyIcon)
        {
            if (e.NewValue == null)
            {
                Unregister(notifyIcon);
            }
            else
            {
                Register(e.NewValue.ToString(), notifyIcon);
            }
        }
    }

    public string Token
    {
        get => (string) GetValue(TokenProperty);
        set => SetValue(TokenProperty, value);
    }

    public static readonly DependencyProperty TextProperty = DependencyProperty.Register(
        nameof(Text), typeof(string), typeof(NotifyIcon), new PropertyMetadata(default(string)));

    public string Text
    {
        get => (string) GetValue(TextProperty);
        set => SetValue(TextProperty, value);
    }

    public static readonly DependencyProperty IconProperty = DependencyProperty.Register(
        nameof(Icon), typeof(ImageSource), typeof(NotifyIcon), new PropertyMetadata(default(ImageSource), OnIconChanged));

    private static void OnIconChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var ctl = (NotifyIcon) d;
        ctl._icon = (ImageSource) e.NewValue;
        ctl.OnIconChanged();

        if (!string.IsNullOrEmpty(ctl._windowClassName) && !ctl.IsBlink && ctl.Visibility == Visibility.Visible)
        {
            ctl.UpdateIcon(true);
        }
    }

    public ImageSource Icon
    {
        get => (ImageSource) GetValue(IconProperty);
        set => SetValue(IconProperty, value);
    }

    public static readonly DependencyProperty ContextContentProperty = DependencyProperty.Register(
        nameof(ContextContent), typeof(object), typeof(NotifyIcon), new PropertyMetadata(default(object)));

    public object ContextContent
    {
        get => GetValue(ContextContentProperty);
        set => SetValue(ContextContentProperty, value);
    }

    public static readonly DependencyProperty BlinkIntervalProperty = DependencyProperty.Register(
        nameof(BlinkInterval), typeof(TimeSpan), typeof(NotifyIcon), new PropertyMetadata(TimeSpan.FromMilliseconds(500), OnBlinkIntervalChanged));

    private static void OnBlinkIntervalChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var ctl = (NotifyIcon) d;
        if (ctl._dispatcherTimerBlink != null)
        {
            ctl._dispatcherTimerBlink.Interval = (TimeSpan) e.NewValue;
        }
    }

    public TimeSpan BlinkInterval
    {
        get => (TimeSpan) GetValue(BlinkIntervalProperty);
        set => SetValue(BlinkIntervalProperty, value);
    }

    public static readonly DependencyProperty IsBlinkProperty = DependencyProperty.Register(
        nameof(IsBlink), typeof(bool), typeof(NotifyIcon), new PropertyMetadata(ValueBoxes.FalseBox, OnIsBlinkChanged));

    private static void OnIsBlinkChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var ctl = (NotifyIcon) d;
        if (ctl.Visibility != Visibility.Visible) return;
        if ((bool) e.NewValue)
        {
            if (ctl._dispatcherTimerBlink == null)
            {
                ctl._dispatcherTimerBlink = new DispatcherTimer
                {
                    Interval = ctl.BlinkInterval
                };
                ctl._dispatcherTimerBlink.Tick += ctl.DispatcherTimerBlinkTick;
            }
            ctl._dispatcherTimerBlink.Start();
        }
        else
        {
            ctl._dispatcherTimerBlink?.Stop();
            ctl._dispatcherTimerBlink = null;
            ctl.UpdateIcon(true);
        }
    }

    public bool IsBlink
    {
        get => (bool) GetValue(IsBlinkProperty);
        set => SetValue(IsBlinkProperty, ValueBoxes.BooleanBox(value));
    }

    private void DispatcherTimerBlinkTick(object sender, EventArgs e)
    {
        if (Visibility != Visibility.Visible || _iconCurrentHandle == IntPtr.Zero) return;
        UpdateIcon(true, !_isTransparent);
    }

    private bool CheckMouseIsEnter()
    {
        var isTrue = FindNotifyIcon(out var rectNotifyList);
        if (!isTrue) return false;

        InteropMethods.GetCursorPos(out var point);
        return rectNotifyList.Any(rectNotify =>
            point.X >= rectNotify.Left &&
            point.X <= rectNotify.Right &&
            point.Y >= rectNotify.Top &&
            point.Y <= rectNotify.Bottom);
    }

    private void DispatcherTimerPos_Tick(object sender, EventArgs e)
    {
        if (CheckMouseIsEnter())
        {
            if (!_isMouseOver)
            {
                _isMouseOver = true;
                RaiseEvent(new MouseEventArgs(Mouse.PrimaryDevice, Environment.TickCount)
                {
                    RoutedEvent = MouseEnterEvent
                });
                _dispatcherTimerPos.Interval = TimeSpan.FromMilliseconds(500);
            }
        }
        else
        {
            _dispatcherTimerPos.Stop();
            _isMouseOver = false;
            RaiseEvent(new MouseEventArgs(Mouse.PrimaryDevice, Environment.TickCount)
            {
                RoutedEvent = MouseLeaveEvent
            });
        }
    }

    //referenced from http://www.cnblogs.com/sczmzx/p/5158127.html
    private IntPtr FindTrayToolbarWindow()
    {
        var hWnd = InteropMethods.FindWindow("Shell_TrayWnd", null);
        if (hWnd != IntPtr.Zero)
        {
            hWnd = InteropMethods.FindWindowEx(hWnd, IntPtr.Zero, "TrayNotifyWnd", null);
            if (hWnd != IntPtr.Zero)
            {
                hWnd = InteropMethods.FindWindowEx(hWnd, IntPtr.Zero, "SysPager", null);
                if (hWnd != IntPtr.Zero)
                {
                    hWnd = InteropMethods.FindWindowEx(hWnd, IntPtr.Zero, "ToolbarWindow32", null);
                }
            }
        }

        return hWnd;
    }

    //referenced from http://www.cnblogs.com/sczmzx/p/5158127.html
    private IntPtr FindTrayToolbarOverFlowWindow()
    {
        var hWnd = InteropMethods.FindWindow("NotifyIconOverflowWindow", null);
        if (hWnd != IntPtr.Zero)
        {
            hWnd = InteropMethods.FindWindowEx(hWnd, IntPtr.Zero, "ToolbarWindow32", null);
        }
        return hWnd;
    }

    private bool FindNotifyIcon(out List<InteropValues.RECT> rectList)
    {
        var rectNotifyList = new List<InteropValues.RECT>();
        var hTrayWnd = FindTrayToolbarWindow();
        var isTrue = FindNotifyIcon(hTrayWnd, ref rectNotifyList);
        if (!isTrue)
        {
            hTrayWnd = FindTrayToolbarOverFlowWindow();
            isTrue = FindNotifyIcon(hTrayWnd, ref rectNotifyList);
        }
        rectList = rectNotifyList;
        return isTrue;
    }

    //referenced from http://www.cnblogs.com/sczmzx/p/5158127.html
    private bool FindNotifyIcon(IntPtr hTrayWnd, ref List<InteropValues.RECT> rectNotifyList)
    {
        InteropMethods.GetWindowRect(hTrayWnd, out var rectTray);
        var count = (int) InteropMethods.SendMessage(hTrayWnd, InteropValues.TB_BUTTONCOUNT, 0, IntPtr.Zero);

        var isFind = false;
        if (count > 0)
        {
            InteropMethods.GetWindowThreadProcessId(hTrayWnd, out var trayPid);
            var hProcess = InteropMethods.OpenProcess(InteropValues.ProcessAccess.VMOperation | InteropValues.ProcessAccess.VMRead | InteropValues.ProcessAccess.VMWrite, false, trayPid);
            var address = InteropMethods.VirtualAllocEx(hProcess, IntPtr.Zero, 1024, InteropValues.AllocationType.Commit, InteropValues.MemoryProtection.ReadWrite);

            var btnData = new InteropValues.TBBUTTON();
            var trayData = new InteropValues.TRAYDATA();
            var handle = Process.GetCurrentProcess().Id;

            for (uint i = 0; i < count; i++)
            {
                InteropMethods.SendMessage(hTrayWnd, InteropValues.TB_GETBUTTON, i, address);
                var isTrue = InteropMethods.ReadProcessMemory(hProcess, address, out btnData, Marshal.SizeOf(btnData), out _);
                if (!isTrue) continue;

                if (btnData.dwData == IntPtr.Zero)
                {
                    btnData.dwData = btnData.iString;
                }

                InteropMethods.ReadProcessMemory(hProcess, btnData.dwData, out trayData, Marshal.SizeOf(trayData), out _);
                InteropMethods.GetWindowThreadProcessId(trayData.hwnd, out var dwProcessId);

                if (dwProcessId == (uint) handle)
                {
                    var rect = new InteropValues.RECT();
                    var lngRect = InteropMethods.VirtualAllocEx(hProcess, IntPtr.Zero, Marshal.SizeOf(typeof(Rect)), InteropValues.AllocationType.Commit, InteropValues.MemoryProtection.ReadWrite);

                    InteropMethods.SendMessage(hTrayWnd, InteropValues.TB_GETITEMRECT, i, lngRect);
                    InteropMethods.ReadProcessMemory(hProcess, lngRect, out rect, Marshal.SizeOf(rect), out _);

                    InteropMethods.VirtualFreeEx(hProcess, lngRect, Marshal.SizeOf(rect), InteropValues.FreeType.Decommit);
                    InteropMethods.VirtualFreeEx(hProcess, lngRect, 0, InteropValues.FreeType.Release);

                    var left = rectTray.Left + rect.Left;
                    var top = rectTray.Top + rect.Top;
                    var botton = rectTray.Top + rect.Bottom;
                    var right = rectTray.Left + rect.Right;
                    rectNotifyList.Add(new InteropValues.RECT
                    {
                        Left = left,
                        Right = right,
                        Top = top,
                        Bottom = botton
                    });
                    isFind = true;
                }
            }
            InteropMethods.VirtualFreeEx(hProcess, address, 0x4096, InteropValues.FreeType.Decommit);
            InteropMethods.VirtualFreeEx(hProcess, address, 0, InteropValues.FreeType.Release);
            InteropMethods.CloseHandle(hProcess);
        }
        return isFind;
    }

    private void OnIconChanged()
    {
        if (_windowClassName == null) return;

        if (_icon != null)
        {
            IconHelper.GetIconHandlesFromImageSource(_icon, out _, out _iconHandle);
            _iconCurrentHandle = _iconHandle.CriticalGetHandle();
        }
        else
        {
            if (_iconDefaultHandle == IntPtr.Zero)
            {
                IconHelper.GetDefaultIconHandles(out _, out _iconHandle);
                _iconDefaultHandle = _iconHandle.CriticalGetHandle();
            }
            _iconCurrentHandle = _iconDefaultHandle;
        }
    }

    private void UpdateIcon(bool showIconInTray, bool isTransparent = false)
    {
        lock (_syncObj)
        {
            if (DesignerHelper.IsInDesignMode) return;

            _isTransparent = isTransparent;
            var data = new InteropValues.NOTIFYICONDATA
            {
                uCallbackMessage = WmTrayMouseMessage,
                uFlags = InteropValues.NIF_MESSAGE | InteropValues.NIF_ICON | InteropValues.NIF_TIP,
                hWnd = _messageWindowHandle,
                uID = _id,
                dwInfoFlags = InteropValues.NIF_TIP,
                hIcon = isTransparent ? IntPtr.Zero : _iconCurrentHandle,
                szTip = Text
            };

            if (showIconInTray)
            {
                if (!_added)
                {
                    InteropMethods.Shell_NotifyIcon(InteropValues.NIM_ADD, data);
                    _added = true;
                }
                else
                {
                    InteropMethods.Shell_NotifyIcon(InteropValues.NIM_MODIFY, data);
                }
            }
            else if (_added)
            {
                InteropMethods.Shell_NotifyIcon(InteropValues.NIM_DELETE, data);
                _added = false;
            }
        }
    }

    private void RegisterClass()
    {
        _windowClassName = $"HandyControl.Controls.NotifyIcon{Guid.NewGuid()}";
        var wndclass = new InteropValues.WNDCLASS4ICON
        {
            style = 0,
            lpfnWndProc = _callback,
            cbClsExtra = 0,
            cbWndExtra = 0,
            hInstance = IntPtr.Zero,
            hIcon = IntPtr.Zero,
            hCursor = IntPtr.Zero,
            hbrBackground = IntPtr.Zero,
            lpszMenuName = "",
            lpszClassName = _windowClassName
        };

        InteropMethods.RegisterClass(wndclass);
        _wmTaskbarCreated = InteropMethods.RegisterWindowMessage("TaskbarCreated");
        _messageWindowHandle = InteropMethods.CreateWindowEx(0, _windowClassName, "", 0, 0, 0, 1, 1,
            IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);
    }

    private IntPtr Callback(IntPtr hWnd, int msg, IntPtr wparam, IntPtr lparam)
    {
        if (msg == _wmTaskbarCreated)
        {
            if (_messageWindowHandle == hWnd && Visibility == Visibility.Visible)
            {
                UpdateIcon(false);
                UpdateIcon(true);
            }
        }
        else
        {
            switch (lparam.ToInt64())
            {
                case InteropValues.WM_LBUTTONDBLCLK:
                    WmMouseDown(MouseButton.Left, 2);
                    break;
                case InteropValues.WM_LBUTTONUP:
                    WmMouseUp(MouseButton.Left);
                    break;
                case InteropValues.WM_RBUTTONUP:
                    ShowContextMenu();
                    WmMouseUp(MouseButton.Right);
                    break;
                case InteropValues.WM_MOUSEMOVE:
                    if (!_dispatcherTimerPos.IsEnabled)
                    {
                        _dispatcherTimerPos.Interval = TimeSpan.FromMilliseconds(200);
                        _dispatcherTimerPos.Start();
                    }
                    break;
            }
        }

        return InteropMethods.DefWindowProc(hWnd, msg, wparam, lparam);
    }

    private void WmMouseDown(MouseButton button, int clicks)
    {
        if (clicks == 2)
        {
            RaiseEvent(new MouseButtonEventArgs(Mouse.PrimaryDevice, Environment.TickCount, button)
            {
                RoutedEvent = MouseDoubleClickEvent
            });
            _doubleClick = true;
        }
    }

    private void WmMouseUp(MouseButton button)
    {
        if (!_doubleClick && button == MouseButton.Left)
        {
            RaiseEvent(new MouseButtonEventArgs(Mouse.PrimaryDevice, Environment.TickCount, button)
            {
                RoutedEvent = ClickEvent
            });
        }
        _doubleClick = false;
    }

    private void ShowContextMenu()
    {

        if (ContextContent != null)
        {
            _contextContent ??= new Popup
            {
                Placement = PlacementMode.Mouse,
                AllowsTransparency = true,
                StaysOpen = false,
                UseLayoutRounding = true,
                SnapsToDevicePixels = true
            };

            _contextContent.Child = new ContentControl
            {
                Content = ContextContent
            };
            _contextContent.IsOpen = true;
            InteropMethods.SetForegroundWindow(_contextContent.Child.GetHandle());
        }
        else if (ContextMenu != null)
        {
            if (ContextMenu.Items.Count == 0) return;

            ContextMenu.InvalidateProperty(StyleProperty);
            foreach (var item in ContextMenu.Items)
            {
                if (item is MenuItem menuItem)
                {
                    menuItem.InvalidateProperty(StyleProperty);
                }
                else
                {
                    var container = ContextMenu.ItemContainerGenerator.ContainerFromItem(item) as MenuItem;
                    container?.InvalidateProperty(StyleProperty);
                }
            }

            ContextMenu.Placement = PlacementMode.Mouse;
            ContextMenu.IsOpen = true;

            InteropMethods.SetForegroundWindow(ContextMenu.GetHandle());
        }
    }

    public static readonly RoutedEvent ClickEvent =
        EventManager.RegisterRoutedEvent("Click", RoutingStrategy.Bubble,
            typeof(RoutedEventHandler), typeof(NotifyIcon));

    public event RoutedEventHandler Click
    {
        add => AddHandler(ClickEvent, value);
        remove => RemoveHandler(ClickEvent, value);
    }

    public static readonly RoutedEvent MouseDoubleClickEvent =
        EventManager.RegisterRoutedEvent("MouseDoubleClick", RoutingStrategy.Bubble,
            typeof(RoutedEventHandler), typeof(NotifyIcon));

    public event RoutedEventHandler MouseDoubleClick
    {
        add => AddHandler(MouseDoubleClickEvent, value);
        remove => RemoveHandler(MouseDoubleClickEvent, value);
    }

    private void UpdateDataContext(FrameworkElement target, object oldValue, object newValue)
    {
        if (target == null || BindingOperations.GetBindingExpression(target, DataContextProperty) != null) return;
        if (ReferenceEquals(this, target.DataContext) || Equals(oldValue, target.DataContext))
        {
            target.DataContext = newValue ?? this;
        }
    }

    private void Dispose(bool disposing)
    {
        if (_isDisposed) return;
        if (disposing)
        {
            if (_dispatcherTimerBlink != null && IsBlink)
            {
                _dispatcherTimerBlink.Stop();
            }
            UpdateIcon(false);
        }

        _isDisposed = true;
    }
}
